package cn.wolfcode.service.impl;

import cn.wolfcode.common.exception.BusinessException;
import cn.wolfcode.common.utils.AssertUtil;
import cn.wolfcode.common.utils.IdGenerateUtil;
import cn.wolfcode.web.msg.SeckillCodeMsg;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class DistributedLockSupport {

    private final StringRedisTemplate redisTemplate;
    private final RedisScript<Boolean> lockScript;
    private final RedisScript<Boolean> unlockScript;
    private final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;

    public DistributedLockSupport(StringRedisTemplate redisTemplate, RedisScript<Boolean> lockScript, RedisScript<Boolean> unlockScript, ScheduledThreadPoolExecutor scheduledThreadPoolExecutor) {
        this.redisTemplate = redisTemplate;
        this.lockScript = lockScript;
        this.unlockScript = unlockScript;
        this.scheduledThreadPoolExecutor = scheduledThreadPoolExecutor;
    }

    public void lock(String lockKey, int timeout, BusinessHandler businessHandler) {
        //自定义分布式锁思路
        //1.锁哪个对象 =》 锁指定场次下的指定商品
        //2.当多线程同时加锁，只能有一个线程加锁成功 =》 Redis的setnx命令(key存在则不做，key不存在则保存)，如：使用 setnx 秒杀场次+秒杀商品id xxx
        //3.锁记录在什么地方 => 利用Redis的 setnx 命令存储在Redis的String数据结构中
        //雪花算法：生成分布式唯一id，作为线程的id,来保证自己释放自己的分布式锁
        String threadId = IdGenerateUtil.get().nextId() + "";
        ScheduledFuture<?> future = null;
        try {
            int count = 0;
            int max = 5;
            //4.当线程获取不到锁的时候，执行什么策略 => 阻塞/自旋等待(限制次数)/直接抛异常
            //如果自旋次数超过5次，抛出异常
            do {
                //加锁
//                Boolean ret = redisTemplate.opsForValue().setIfAbsent(key, "1");
                //避免死锁的问题：使用Lua脚本，一次性发送 setnx 和 设置过期时间的命令给redis 执行
                Boolean ret = redisTemplate.execute(lockScript, Collections.singletonList(lockKey), threadId, timeout + "");
                if (ret != null && ret) {
                    break;
                }
                AssertUtil.isTrue((count++) < max, SeckillCodeMsg.SECKILL_ERROR.getMsg());
                //避免CPU过于频繁
                TimeUnit.MILLISECONDS.sleep(20);
            } while (true);

            //加锁成功后，创建 WatchDog 看门狗监听 业务是否执行完成(Redis中的key是否存在)，实现续期操作
            long delayTime = (long) (timeout * 0.8);
            log.debug("[WatchDog]--------------- 间隔时间：{}", delayTime);
            future = scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> {
                //从redis查询key是否存在，如果存在，则续期
                String value = redisTemplate.opsForValue().get(lockKey);
                if (StringUtils.isNotEmpty(value)) {
                    redisTemplate.expire(lockKey, delayTime + 2, TimeUnit.SECONDS);
                    log.debug("[WatchDog] --------------------执行Redis实现续期操作 threadId={} key={} value={}", threadId, lockKey, value);
                }
            }, delayTime, delayTime, TimeUnit.SECONDS);
            //业务逻辑代码-----------------------------开始
            businessHandler.handle();
            //业务逻辑代码------------------------------结束
        } catch (Exception e) {
            e.printStackTrace();
            throw new BusinessException(SeckillCodeMsg.SECKILL_ERROR);
        } finally {
            //业务执行完成，则销毁key续期操作任务，取消WatchDog监听
            if (future != null) {
                //如果该任务还在执行，是否停止。如果走到finally代表业务逻辑已经执行完成，因此不需要该任务去执行续期操作了
                future.cancel(true);
            }
            //释放锁
            redisTemplate.execute(unlockScript, Collections.singletonList(lockKey), threadId);
        }
    }

    @FunctionalInterface
    public static interface BusinessHandler {
        void handle();
    }
}
